#!/bin/bash
set -o errexit

# 用法说明
function usage(){
    echo "usage:"
    echo "  $0 <options>"
    echo "     -n|--vm-name <VMNAME> 要clone的虚拟机名称(alias)"
    echo "     --uuid <VMUUID> 要clone的虚拟机uuid，如果设置此参数，则--vm-name|-n参数失效"
    echo "     --vm-snapshot <VMSNAPSHOT> 要clone的虚拟机快照，可选，如果设置此参数，则从此快照处克隆，快照实际名称为：vmsnap-<SNAPSHOT>，默认快照名为： migration"
    echo "     -tuuid|--target-uuid <TARGETUUID> 新虚拟机的uuid,可选,如果不指定，生成一新uuid"
    echo "     -tn|--target-name <TARGETNAME> 新虚拟机的名称,可选,如果不指定，同新虚拟机的uuid"
    echo "     -tip|--target-ip <TARGETIP> 新虚拟机网卡的ip,可选，如果不指定，默认为dhcp，只在虚拟机仅有一个网卡时起作用"
    echo "     --force 当快照已存在时，用已有快照迁移, 可选，如果没有设置，默认为快照存在时报错退出"
    echo "     --help|-h 显示使用帮助，可选"
    echo ""
    echo "     本脚本在SmartOS(非Triton)下运行"
    echo "     暂时只支持在同一主机中进行虚拟机克隆"
    echo "     对于拥有filesystems的虚拟机，会直接挂载源虚拟机的目录（即和源虚拟机挂载相同的主机目录），可能会造成异常"
    echo "     对于没有指定的--vm-snapshot的源虚拟机快照，克隆完成后会进行移除，即使是源虚拟机已存在的快照（指定--force参数时可能会出现这种情况)"
    echo "     对于kvm虚拟机克隆，没有自动启动，请手动启动（首次需要等比较长的时间后才能成功启动）"
    echo "     如果克隆失败，需要先删除不成功的新虚拟机后再进行操作"
    echo "     本脚本原则上不会对原始zone进行修改，不保证使用本脚本可能造成的数据损坏，使用时请自行评估风险"
    echo ""
    echo "     使用示例："
    echo "          $0 --uuid 1e65c912-3123-11eb-a066-4338f21c17e7"
    echo "              克隆uuid为1e65c912-3123-11eb-a066-4338f21c17e7的虚拟机, 新虚拟机名称为新虚拟机的uuid"
    echo "          $0 -n web01"
    echo "              克隆名为web01的虚拟机, 新虚拟机名称为新虚拟机的uuid"      
    echo "          $0 -n web01 -tn web02"
    echo "              克隆名为web01的虚拟机, 新虚拟机名称为web02"
    echo "          $0 -n web01 -tn web02 -tuuid a261a03e-3122-11eb-963a-fb1d5467d60c"
    echo "              克隆名为web01的虚拟机, 新虚拟机名称为web02, 新虚拟机的uuid指定为 a261a03e-3122-11eb-963a-fb1d5467d60c"
    echo "          $0 -n web01 -tn web02 -tip 192.168.1.168"
    echo "              克隆名为web01的虚拟机, 新虚拟机名称为web02, 新虚拟机的ip指定为 192.168.1.168(仅在虚拟机只有一个网卡时起作用"
    echo "          $0 -n web01 -tn web02 --vm-snapshot 20201120"
    echo "              从名为web01的虚拟机的快照vmsnap-20201120处克隆虚拟机, 新虚拟机名称为web02, 这在某些情况下非常有用"
    echo "          $0 -n web01 -tn web02 --force"
    echo "              克隆名为web01的虚拟机, 新虚拟机名称为web02, 如果快照已存在，用原来的快照进行克隆（这有可能造成克隆的不是当前虚拟机最新状态，在某些情况下有用）"
    echo "          $0 -h"
    echo "              显示命令使用帮助"
    exit
}

# 出错提示
function fatal(){
    echo ""
    echo "$1"
    exit 1
}

# 获取虚拟机image_uuid
function get_image_uuid(){
    local imageuuid=""
    if [ "${vm_brand}" == "kvm" -o "${vm_brand}" == "bhyve" ]; then
            imageuuid=$(vmadm get ${vm_uuid}|json disks.0.image_uuid)
    else
            imageuuid=$(vmadm get ${vm_uuid}|json image_uuid)
    fi
    echo "${imageuuid}"
}

function count_speed(){
    local started_at=$1
    
    local elapsed=$(($(date +%s) - ${started_at}))
    
    local msg="cost: ${elapsed}s"        
    
    local p_size=$2
    if [ "${p_size}" != "" ]; then
        local p_speed=5
        local p_unit='MB/s'
        if [ "${elapsed}" != "0" ];then
            p_speed=$((p_size / elapsed))
            if [[ $p_speed -gt 1024*1024 ]];then
                p_speed=$(awk 'BEGIN{printf("%.2f",'$p_size' / '$elapsed' / 1024 / 1024)}')
                p_unit='MB/s'
            elif [ $p_speed -gt 1024 ];then
                p_speed=$(awk 'BEGIN{printf("%.2f",'$p_size' / '$elapsed' / 1024)}')
                p_unit='KB/s'
            else
                p_speed=$(awk 'BEGIN{printf("%.2f",'$p_size' / '$elapsed')}')
                p_unit='B/s'
            fi
        fi
        msg="${p_speed}${p_unit} cost: ${elapsed}s"
    fi
    echo "${msg}"
}

# 程序执行开始时间
BEGIN_AT=$(date +%s)
# 如果快照存在，是否用现有快照克隆
force_clone=false

# 选项处理
while [[ $# -ge 1 ]]; do
    case $1 in
        -n|--vm-name )
                vm_name=$2
                shift 2
                ;;
        --uuid )
                vm_uuid=$2
                shift 2
                ;;
        --vm-snapshot )
                vm_snapshot="vmsnap-$2"
                shift 2
                ;;
        -tn|--target-name )
                target_name=$2
                shift 2
                ;;
        -tuuid|--target-uuid )
                target_uuid=$2
                shift 2
                ;;
        -tip|--target-ip )
                target_ip=$2
                shift 2
                ;;
        -h|--help )
                usage
                ;;
        --force )
                force_clone=true
                shift
                ;;
        * )
                usage
                ;;
    esac
done

# 预处理源虚拟机
if [ "${vm_uuid}" == "" ]; then
    if [ "${vm_name}" == "" ]; then
        echo "没有提供vm uuid 或 vm name"
        usage
    fi
    vm_uuid=$(vmadm list -Ho uuid alias=${vm_name})
    [[ "${vm_uuid}" == "" ]] && fatal "不存在alias为${vm_name}的虚拟机"
else
    vm_name=$(vmadm list -Ho alias uuid=${vm_uuid})
    [[ "${vm_name}" == "" ]] && fatal "不存在uuid为${vm_uuid}的虚拟机"
fi

# 源虚拟机brand
vm_brand=$(vmadm get ${vm_uuid}|json brand)
# 源虚拟机类型， os / hvm
vm_type="os"
if [ "${vm_brand}" == "kvm" -o "${vm_brand}" == "bhyve" ];then
    vm_type="hvm"
fi

# 预处理目标虚拟机
if [ "${target_uuid}" == "" ]; then
    target_uuid=$(uuid)
else
    [[ "$(vmadm list -Ho alias uuid=${target_uuid})" == "" ]] || fatal "已存在uuid为${target_uuid}的虚拟机"
fi

if [ "${target_name}" == "" ]; then
    target_name=${target_uuid}
else
    [[ "$(vmadm list -Ho uuid alias=${target_name})" == "" ]] || fatal "已存在alias为${target_name}的虚拟机"
fi


# 获取虚拟机镜像
image_uuid="$(get_image_uuid)"
[[ "${image_uuid}" == "" ]] && fatal "虚拟机${vm_uuid}[${vm_name}]不存在image_uuid"
# 虚假机镜像final快照
image_origin_snapshot="zones/${image_uuid}@final"

echo ""
echo "克隆 zone[${vm_brand}] ${vm_uuid}[${vm_name}] ==> ${target_uuid}[${target_name}] image_uuid=${image_uuid}"
echo ""

# hvm磁盘需要独立处理，判断是否有不规范全名的磁盘
# kvm磁盘规范全名： zones/<uuid>-diskX, 其中<uuid>为虚拟机uuid, X为数字序号，从0开始，0表示系统盘
# bhyve磁盘规范全名： zones/<uuid>/diskX, 其中<uuid>为虚拟机uuid, X为数字序号，从0开始，0表示系统盘
if [ "${vm_type}" == "hvm" ]; then
    disks=$(vmadm get ${vm_uuid}|json disks|json -a zfs_filesystem)
    for disk in ${disks};
    do
        newdisk=${disk/${vm_uuid}/${target_uuid}}
        [[ "${disk}" == "${newdisk}" ]] && fatal "disk ${disk} 不是规范命名磁盘"
    done
fi

# 是否需要替换IP地址, 默认不替换， TODO: 默认子网掩码为24
replace_ip=false
if [ "${target_ip}" != "" ]; then
    [[ $(vmadm get ${vm_uuid}|json nics | json -a ip|wc -l) -eq 1 ]] && replace_ip=true
fi

# 虚拟机名称 base64编码
vm_name_base64=$(echo -n ${vm_name}| openssl base64)
# 目标虚拟机名称 base64编码
target_name_base64=$(echo -n ${target_name}| openssl base64)

# 目标虚拟机zone配置文件
zonecfgfile=/tmp/${target_uuid}.zcfg

# 导出zone配置
zonecfg -z ${vm_uuid} export > ${zonecfgfile}
# 替换uuid
sed -i '' "s/${vm_uuid}/${target_uuid}/g" ${zonecfgfile}
# 替换虚拟机名称
sed -i '' "s/${vm_name_base64}/${target_name_base64}/g" ${zonecfgfile}


# 删除nic mac地址, 防止mac地址冲突，不设置mac地址时SmartOS会自动生成一个mac地址
sed -i '' "/set mac-addr=/d" ${zonecfgfile}
# 替换IP
if [[ ${replace_ip} == false ]]; then
    sed -i '' "s/name=ip,value=\".*\"/name=ip,value=\"dhcp\"/g" ${zonecfgfile}
    sed -i '' "s/name=ips,value=\".*\"/name=ips,value=\"dhcp\"/g" ${zonecfgfile}
else 
    sed -i '' "s/name=ip,value=\".*\"/name=ip,value=\"${target_ip}\"/" ${zonecfgfile}
    sed -i '' "s/name=ips,value=\".*\"/name=ips,value=\"${target_ip}\/24\"/" ${zonecfgfile}
fi

# bhyve快照必须要有flexible_disk_size可用空间，如果没有设置，默认增加2GB空余空间
if [ "${vm_brand}" == "bhyve" ]; then
    flexible_disk_size=$(vmadm get ${vm_uuid} | json flexible_disk_size)
    if [ "${flexible_disk_size}" == "" ]; then
        quota_g=$(zfs get -Ho value quota zones/${vm_uuid})
        quota_m=$(echo "(${quota_g%?}+2)*1024"|bc)
        echo "临时设置flexible_disk_size=${quota_m%.*}"
        vmadm update ${vm_uuid} flexible_disk_size=${quota_m%.*}
    fi
fi


echo -n "检查快照..."
# 克隆快照名
snapshot=migration
# 是否指定了虚拟机快照(非kvm虚拟机)，用于从指定快照克隆虚拟机
if [ "${vm_snapshot}" != "" ]; then
    # 使用指定的快照并判断快照是否存在
    snapshot=${vm_snapshot}
    ok="$(zfs list -Ht snapshot -o name zones/${vm_uuid}@${snapshot} 2> /dev/null||true)"
    [[ "${ok}" == "" ]] && fatal "虚拟机${vm_uuid}[${vm_name}]快照: zones/${vm_uuid}@${snapshot} 不存在"
else
    # 判断快照是否存在
    ok="$(zfs list -Ht snapshot -o name zones/${vm_uuid}@${snapshot} 2> /dev/null||true)"
    if [ "${ok}" == "" ]; then
        zfs snapshot -r zones/${vm_uuid}@${snapshot}
    else
        # 如果快照存在且force_clone不为true, 提示并退出，以防止意外克隆
        [[ ${force_clone} == true ]] || fatal "虚拟机${vm_uuid}[${vm_name}]快照: zones/${vm_uuid}@${snapshot} 已存在"
    fi
fi

# kvm虚拟机磁盘建立快照
if [ "${vm_brand}" == "kvm" ]; then
    for disk in ${disks};
    do
        ok="$(zfs list -Ht snapshot -o name ${disk}@${snapshot} 2> /dev/null||true)"
        if [ "${ok}" == "" ]; then
            zfs snapshot ${disk}@${snapshot}
        else
            [[ ${force_clone} == true ]] || fatal "虚拟机${vm_uuid}[${vm_name}]磁盘快照: ${disk}@${snapshot} 已存在"
        fi
    done
fi
echo " DONE"


echo -n "导入zone config..."
# 导入目标zone配置
zonecfg -z ${target_uuid} -f ${zonecfgfile}
echo " DONE"

echo -n "发送zone快照 @${snapshot}..."
STARTED_AT=$(date +%s)
# 发送快照到目标虚拟机
if [ "${vm_type}" == "os" ]; then
    echo -n " 增量发送"
    zfs send -R -I ${image_origin_snapshot} zones/${vm_uuid}@${snapshot} | zfs recv zones/${target_uuid}
else
    echo -n " 全量发送"
    zfs send zones/${vm_uuid}@${snapshot} | zfs recv zones/${target_uuid}    
fi
echo " DONE $(count_speed $STARTED_AT)"


# 如果是hvm，发送磁盘快照到目标虚拟机
if [ "${vm_type}" == "hvm" ]; then
    echo  "发送${vm_brand}磁盘快照 @${snapshot}..."
    for disk in ${disks};
    do
        newdisk=${disk/${vm_uuid}/${target_uuid}}
        STARTED_AT=$(date +%s)
        if [ "$(zfs get -Ho value origin ${disk})" == "${image_origin_snapshot}" ]; then
                echo -n "disk ${disk}@${snapshot} 增量发送..."
                zfs send -I ${image_origin_snapshot} ${disk}@${snapshot} | zfs recv ${newdisk}
                echo " DONE $(count_speed $STARTED_AT)"
        else
                echo -n "disk ${disk}@${snapshot} 全量发送..."
                zfs send ${disk}@${snapshot} | zfs recv ${newdisk}
                echo " DONE $(count_speed $STARTED_AT)"
        fi
    done
fi

echo -n "安装 zone ${target_uuid}..."
STARTED_AT=$(date +%s)
zoneadm -z ${target_uuid} attach
echo " DONE $(count_speed $STARTED_AT)"

echo -n "清理临时数据..."
STARTED_AT=$(date +%s)
# 清除目标虚拟机zone配置文件 
rm -f ${zonecfgfile}
# 清除源虚拟机临时快照
[[ "${vm_snapshot}" == "" ]] && zfs destroy -r zones/${vm_uuid}@${snapshot}

# 清除kvm源虚拟机的磁盘临时快照
if [ "${vm_brand}" == "kvm" ]; then
    for disk in ${disks};
    do
            zfs destroy ${disk}@${snapshot}
    done
fi
echo " DONE $(count_speed $STARTED_AT)"

# 如果原始bhyve虚拟机没有设置flexible_disk_size，则重置flexible_disk_size为空
if [ "${vm_brand}" == "bhyve" -a "${flexible_disk_size}" == "" ]; then
    vmadm update ${vm_uuid} flexible_disk_size=
fi

echo ""
echo "克隆虚拟机成功! 新虚拟机的uuid为: ${target_uuid}"
echo "总用时: $(count_speed $BEGIN_AT) ====================================================="
echo ""
if [ "${vm_brand}" == "kvm" ]; then
    echo "请手动启动新虚拟机 vmadm start ${target_uuid}"
else
    echo "启动新虚拟机 ${target_uuid}..."
    vmadm start ${target_uuid}
    # 如果虚拟机为os,判断是否有delegate_dataset，如有，清除默认源虚拟机挂载目录
    if [ "${vm_type}" == "os" ]; then
        if [ "$(vmadm get ${vm_uuid}|json datasets|json -a)" != "" -a -d "/zones/${target_uuid}/root/zones/${vm_uuid}" ]; then
            rm -rf /zones/${target_uuid}/root/zones/${vm_uuid}
            [[ "$(ls -A /zones/${target_uuid}/root/zones)" == "" ]] && rm -rf /zones/${target_uuid}/root/zones
        fi
    fi
fi
